﻿
Imports System.Threading
Imports std

Public MustInherit Class visitCounter
    Inherits requestProcessor

#If RETIRED Then
    Public Const _pathidentity As String = "/counter/"
    Public Const keyquery As String = "key"
#End If
    Public ReadOnly keySelectionGrammar As String = _
          "the grammar of key selection is, " + newline.incode() _
        + "if is 'referer' or 'counter' or 'count', use referer." + newline.incode() _
        + "if is 'site' or 'host', use site/host in referer." + newline.incode() _
        + "if is 'sitepath' or 'hostpath', use site/host + path in referer." + newline.incode() _
        + "otherwise, use all key selection string as key."

    Public Const _pathidentity As String = "/"

    Public Function refererkey(ByVal path As String) As Boolean
        Return strsame(path, strlen(pathidentity()), "referer", 0, strlen("referer"), False) _
                OrElse strsame(path, strlen(pathidentity()), "counter", 0, strlen("counter"), False) _
                OrElse strsame(path, strlen(pathidentity()), "count", 0, strlen("count"), False)
    End Function

    Public Function sitepathkey(ByVal path As String) As Boolean
        Return strsame(path, strlen(pathidentity()), "sitepath", 0, strlen("sitepath"), False) _
                OrElse strsame(path, strlen(pathidentity()), "hostpath", 0, strlen("hostpath"), False)
    End Function

    Public Function sitekey(ByVal path As String) As Boolean
        Return strsame(path, strlen(pathidentity()), "site", 0, strlen("site"), False) _
                OrElse strsame(path, strlen(pathidentity()), "host", 0, strlen("host"), False)
    End Function

    Public Function refererkey(ByVal context As httpContext) As Boolean
        assert(Not context Is Nothing, "context is nothing.")
        Return refererkey(context.url().path)
    End Function

    Public Function sitepathkey(ByVal context As httpContext) As Boolean
        assert(Not context Is Nothing, "context is nothing.")
        Return sitepathkey(context.url().path)
    End Function

    Public Function sitekey(ByVal context As httpContext) As Boolean
        assert(Not context Is Nothing, "context is nothing.")
        Return sitekey(context.url().path)
    End Function

    Private counters As map(Of String, Int64) = Nothing
    Private changed As setqueue(Of String) = Nothing
    Private timer3Index As Guid = Guid.Empty

    Protected MustOverride Function read(ByVal key As String, ByRef count As Int64) As Boolean
    Protected MustOverride Function write(ByVal key As String, ByVal count As Int64) As Boolean
    Protected MustOverride Function processOutput(ByVal context As httpContext, ByVal count As Int64) As Boolean
#If RETIRED Then
    Protected MustOverride Function processInvalidKey(ByVal context As httpContext, ByVal key As String) As Boolean
#End If
    Protected MustOverride Function processHomepage(ByVal context As httpContext) As Boolean

#If RETIRED Then
    Protected Overridable Function invalidKey(ByVal key As String) As Boolean
        Return isemptystring(key)
    End Function
#End If

    Public Overloads Sub initial(ByVal timer3Interval As Int64)
        MyBase.initial()
        counters = New map(Of String, Int64)()
        changed = New setqueue(Of String)()
        timer3Index = timer3.addcall(AddressOf writeback, timer3Interval)
        assert(timer3Index <> Guid.Empty, "cannot get valid timer3 index.")
    End Sub

    Public Overrides Sub initial(Optional ByVal config As configParser = Nothing)
        MyBase.initial(config)
        If isdebugmode() Then
            initial(1000)
        Else
            initial(16000)
        End If
    End Sub

    Public Overrides Function pathidentity() As String
        Return _pathidentity
    End Function

    Private Delegate Sub operationDelegate(ByRef count As Int64)

    Private Overloads Function process(ByVal context As httpContext, ByVal key As String _
                                         , ByVal operation As operationDelegate) As Boolean
#If RETIRED Then
        If invalidKey(key) Then
            Return processInvalidKey(context, key)
        End If
#End If

        Dim needRead As Boolean = False
        SyncLock counters
            needRead = (counters.find(key) = counters.end())
        End SyncLock

        Dim count As Int64 = 0
        If needRead Then
            If Not read(key, count) Then
                raiseError("cannot read key " + key + ", set as new key.", errorHandle.errorType.application)
                count = 0
            End If
        End If

        'do not trust map will keep iterator after some other operations
        SyncLock counters
            Dim it As map(Of String, Int64).iterator = Nothing
            If needRead Then
                it = counters.insert(key, count)
            Else
                it = counters.find(key)
            End If
            operation((+it).second)
            count = (+it).second
        End SyncLock

        SyncLock changed
            changed.push(key)
        End SyncLock

        If Not processOutput(context, count) Then
            raiseError("cannot processOutput, key " + key + ", ignore request.", errorHandle.errorType.warning)
            Return False
        Else
            context.setcacheControl(False)
            Return True
        End If
    End Function

    'no caller of processDecrease in this class, but leave this api for child class
    Protected Function processDecrease(ByVal context As httpContext, ByVal key As String) As Boolean
        Return process(context, key, AddressOf Interlocked.Decrement)
    End Function

    Protected Function processIncrease(ByVal context As httpContext, ByVal key As String) As Boolean
        Return process(context, key, AddressOf Interlocked.Increment)
    End Function

    Public Overrides Function process(ByVal context As httpContext) As Boolean
        If strsame(context.url().path, pathidentity(), False) Then
            'do not need to set cache control here, sub class will handle it if necessary.
            Return processHomepage(context)
        End If

        Dim key As String = Nothing
#If RETIRED Then
        key = context.url().query(keyquery)
#End If
        If sitekey(context) OrElse sitepathkey(context) OrElse refererkey(context) Then
            If isemptystring(context.referer()) Then
                key = "(null)"
            Else
                If sitekey(context) OrElse sitepathkey(context) Then
                    Dim url As url = Nothing
                    url = New url()
                    url.full() = context.referer()
                    If sitepathkey(context.url().path) Then
                        key = url.host() + url.path()
                    ElseIf sitekey(context.url().path) Then
                        key = url.host()
                    End If
                Else
                    key = context.referer()
                End If
            End If
        Else
            Dim arg As String = Nothing
            arg = strmid(context.url.pathquery(), strlen(pathidentity()))

            Dim index As Int64
            index = strlastindexof(arg, character.dot, 2, False)
            If index <> npos Then
                key = strleft(arg, index)
            Else
                index = strlastindexof(arg, character.dot, False)
                If index <> npos Then
                    key = strleft(arg, index)
                Else
                    copy(key, arg)
                End If
            End If
            'leave last two dots to child class
            debugRaiseError("input argument " + arg + ", key " + key)
        End If
        If isemptystring(key) Then
            'if user set a special referer, and failed to parse by url, we can only set an invalid as key now.
            'as set cannot handle nothing.
            key = "(invalid)"
        End If

        Return processIncrease(context, key)
    End Function

    Protected Overrides Sub closeImpl()
        If Not changed Is Nothing Then
            SyncLock changed
                Dim key As String = Nothing
                While Not changed.empty()
                    key = changed.front()
                    writeback(key)
                    changed.pop()
                End While
            End SyncLock
        End If
        MyBase.closeImpl()
    End Sub

    Private Function writeback(ByVal key As String) As Boolean
        Dim count As Int64
        Dim it As map(Of String, Int64).iterator = Nothing
        Dim success As Boolean = False
        SyncLock counters
            it = counters.find(key)
            If it <> counters.end() Then
                count = (+it).second
                success = True
            End If
        End SyncLock
        If success Then
            If Not write(key, count) Then
                raiseError("cannot write counter back to key " + key, errorHandle.errorType.warning)
                Return False
            Else
                Return True
            End If
        Else
            'do not need to write, though there may some other issues to make such a call
            Return True
        End If
    End Function

    Private Sub writeback()
        Dim key As String = Nothing
        SyncLock changed
            If Not changed.empty() Then
                key = changed.front()
                changed.pop()
            End If
        End SyncLock
        If Not writeback(key) Then
            SyncLock changed
                changed.push(key)
            End SyncLock
        End If
    End Sub

    Protected Overrides Sub Finalize()
        If timer3Index <> Guid.Empty Then
            assert(timer3.removecall(timer3Index), "cannot remove timer3 call for index " + timer3Index.ToString())
            'though it's not a good idea, but still have some situation that a counter instance is not initialized
        End If
        close()
        MyBase.Finalize()
    End Sub
End Class
